/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package fr.machizaud.research.atltransformer;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.emf.ecore.EEnumLiteral;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.eclipse.m2m.atl.core.*;
import org.eclipse.m2m.atl.core.emf.EMFExtractor;
import org.eclipse.m2m.atl.core.emf.EMFInjector;
import org.eclipse.m2m.atl.core.emf.EMFModelFactory;
import org.eclipse.m2m.atl.core.launch.ILauncher;
import org.eclipse.m2m.atl.engine.compiler.AtlStandaloneCompiler;
import org.eclipse.m2m.atl.engine.compiler.atl2006.Atl2006Compiler;
import org.eclipse.m2m.atl.engine.emfvm.launch.EMFVMLauncher;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Andréa
 */
public class ATLEngine {

    private static final Logger logger = Logger.getLogger(ATLEngine.class.getCanonicalName());

    /**
     * Engine injector
     */
    private IInjector injector;
    /**
     * Engine extractor
     */
    private IExtractor extractor;
    /**
     * Model factory
     */
    private ModelFactory factory;

    /**
     * Launcher
     */
    private ILauncher launcher;
    /**
     * engine parameters
     */
    private Map<String, Object> parameters;
    /**
     * Null monitor - ATL engine purpose
     */
    private static final IProgressMonitor NULL_MONITOR = new NullProgressMonitor();

    /**
     * Private class holder for a singleton reference
     */
    private static class ATLEngineHolder {
        private static final ATLEngine instance = new ATLEngine();
    }

    /**
     * Private class holder for a singleton reference
     */
    private static class ATLCompilerHolder {
        private static final AtlStandaloneCompiler instance = new Atl2006Compiler();
    }

    /**
     * private constructor
     */
    private ATLEngine() {
        logger.finer("Creating Injector");
        injector  = new EMFInjector();
        logger.finer("Creating Extractor");
        extractor = new EMFExtractor();
        logger.finer("Creating Model Factory");
        factory = new EMFModelFactory();
        logger.finer("Setting the engine");
        launcher = new EMFVMLauncher();
        launcher.initialize(Collections.<String,Object>emptyMap());
        logger.finer("Initialize parameters");
        parameters = new HashMap<String, Object>();
    }

    /**
     * Unique instance, lazy initialisation
     *
     * @return unique instance
     */
    public static ATLEngine getEngine() {
        return ATLEngineHolder.instance;
    }

    /**
     * Unique instance, lazy initialisation
     *
     * @return unique instance
     */
    public AtlStandaloneCompiler getCompiler() {
        return ATLCompilerHolder.instance;
    }

    /**
     * Transform given model as a resource and print it in given outputStream
     *
     * @param source      resource we want to be transformed
     * @param asmFile     ATL transformation rules files
     * @param printStream where to print the result
     */
    public void transform(
            String inModelFilepath,
            String outModelFilepath,
            String source,
            String asmFile,
            OutputStream printStream
    ) {
        checkRequirement(
                inModelFilepath,
                outModelFilepath,
                source,
                asmFile,
                printStream);
        try {

            logger.finer("Fetching metamodel");
            IReferenceModel inMetaModel  = factory.newReferenceModel();
            IReferenceModel outMetaModel = factory.newReferenceModel();

            injector.inject(
                    inMetaModel,
                    getStream(inModelFilepath),
                    Collections.<String, Object>emptyMap());

            injector.inject(
                    outMetaModel,
                    getStream(outModelFilepath),
                    Collections.<String, Object>emptyMap());

            logger.finer("Creating models template");
            IModel outModel = factory.newModel(outMetaModel);
            IModel inModel  = factory.newModel(inMetaModel);

            logger.finer("Fetching input file");
            URL inFileURL  = getResource(source);

            logger.finer("Map input file to in model");
            injector.inject(inModel, inFileURL.getFile());

            logger.finer("Connect launcher in/out");
            launcher.addInModel(inModel, "IN", getModelName(inModelFilepath));
            launcher.addOutModel(outModel, "OUT", getModelName(outModelFilepath));

            logger.finer("Set the atl file");
            URL atlURL = getResource(asmFile);
            logger.finer("URL : " + atlURL);

            if( atlURL.getFile().endsWith(".atl") ){
                logger.finer("Given file is not compiled");
                atlURL = compile(atlURL);
                logger.finer("New rules file : " + atlURL);
            }

            if( logger.isLoggable(Level.FINER) )
            {
                logger.finer("Trace on");
                parameters.put("step","true");
            }

            logger.finer("Transform");
            launcher.launch(ILauncher.RUN_MODE, NULL_MONITOR, parameters,atlURL.openStream());

            logger.finer("Extract out model");
            parameters.put(XMLResource.OPTION_ENCODING, "UTF-8");
            // Extract the result
            extractor.extract(outModel, printStream, parameters);
        } catch (ATLCoreException ex) {
            Logger.getLogger(ATLEngine.class.getName()).log(Level.SEVERE, "Error during data extraction", ex);
        } catch (IOException ex) {
            Logger.getLogger(ATLEngine.class.getName()).log(Level.SEVERE, "Error on ATL Stream", ex);
        }
    }

    private URL getResource(String source) {
        try {
            return new File(source).toURI().toURL();
        } catch (MalformedURLException e) {
            e.printStackTrace();
            return null;
        }
    }

    private URL compile(URL atlURL) throws IOException {
        String outputAsmFile = atlURL.getFile().substring(
                0,
                atlURL.getFile().length() - 4 //HELP extension size
        ) + ".asm";
        logger.finest("Computing output asm file : " + outputAsmFile);

        logger.finest("Compiling source atl");
        getCompiler().compile(atlURL.openStream(), outputAsmFile);

        logger.finest("Fetching compiled atl rules");
        return getResource(outputAsmFile);
    }

    private void checkRequirement(
            String inModelFilepath,
            String outModelFilepath,
            String source,
            String asmFile,
            OutputStream printStream) {
        try {
        if(
                inModelFilepath.trim().equals("")
                || outModelFilepath.trim().equals("")
                || source.trim().equals("")
                || asmFile.trim().equals("")
                || printStream == null
                )
            throw new IllegalArgumentException("Given parameters are invalid");
        } catch (NullPointerException ex) {
            throw new IllegalArgumentException("Given parameters are invalid");
        }
    }

    private static String getModelName(String modelfilepath) {
        int extIndex = modelfilepath.lastIndexOf('.');
        int separatorIndex = modelfilepath.lastIndexOf(File.separator);

        logger.finer("Model name found : " + modelfilepath.substring(
                separatorIndex < 0 ? 0 : separatorIndex + 1,
                extIndex
        ));
        return modelfilepath.substring(
                separatorIndex < 0 ? 0 : separatorIndex + 1,
                extIndex
        );
    }

    private static InputStream getStream(String filename) {
        InputStream stream = null;
        stream = ATLEngine.class.getClassLoader().getResourceAsStream(filename);
        if( stream != null )
            return stream;
        else
            try {
                stream = new FileInputStream(filename);
                return stream;
            } catch (FileNotFoundException e) {
                return null;
            }

    }
}
